package com.gitee.wsl.android.ui.ext.type

import android.content.Context
import android.graphics.*
import android.graphics.drawable.Drawable
import android.os.Build
import android.renderscript.*
import android.view.View
import androidx.annotation.RequiresApi
import androidx.core.graphics.withTranslation
import androidx.core.view.OneShotPreDrawListener
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.lang.ref.WeakReference
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt


fun Drawable.toBlurredBitmap(
    w: Int,
    h: Int,
    elevation: Float,
    blurMaker: BlurMaker
): Bitmap? {
    val width = (w + elevation * 2).roundToInt()
    val height = (h + elevation * 2).roundToInt()

    return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
        .blurred(blurMaker) {
            withTranslation(elevation, elevation) {
                draw(this)
            }
        }
}

/**
 * Holds required data for of blur operation
 */
data class BlurConfig(
    val width: Int,
    val height: Int,
    val radius: Int = MAX_RADIUS,
    val sampling: Int = DEFAULT_SAMPLING,
    val color: Int = Color.TRANSPARENT
) {
    companion object {
        const val MAX_RADIUS = 25
        const val DEFAULT_SAMPLING = 1
    }
}

//TODO: Make sure if this is being invoked properly
class BlurMaker(context: Context, private val defaultBlurRadius: Int) {

    private val contextRef = WeakReference(context)

    fun blur(
        source: Bitmap,
        radius: Int = defaultBlurRadius,
        sampling: Int = BlurConfig.DEFAULT_SAMPLING
    ): Bitmap? {
        val blurConfig = BlurConfig(
            width = source.width,
            height = source.height,
            radius = radius,
            sampling = sampling
        )
        return blur(source, blurConfig)
    }

    private fun blur(source: Bitmap, blurConfig: BlurConfig): Bitmap? {
        val width = blurConfig.width / blurConfig.sampling
        val height = blurConfig.height / blurConfig.sampling
        if (width == 0 || height == 0) {
            return null
        }
        //create a bitmap
        val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)

        Canvas(bitmap).run {
            scale(1 / blurConfig.sampling.toFloat(), 1 / blurConfig.sampling.toFloat())
            val paint = Paint().apply {
                flags = Paint.FILTER_BITMAP_FLAG or Paint.ANTI_ALIAS_FLAG
                colorFilter = PorterDuffColorFilter(blurConfig.color, PorterDuff.Mode.SRC_ATOP)
            }
            drawBitmap(source, 0f, 0f, paint)
        }

        //try blur with renderscript
        val blurBitmap: Bitmap? = try {
            blurWithRenderScript(bitmap, blurConfig.radius)
        } catch (e: RSRuntimeException) {
            // if renderscript fails, use stack blur
            bitmap.stackBlur(blurConfig.radius)
        }


        // return blurred bitmap
        return blurBitmap?.let {
            if (blurConfig.sampling == BlurConfig.DEFAULT_SAMPLING) {
                it
            } else {
                // If sample size altered, create a scaled bitmap
                val scaled = Bitmap.createScaledBitmap(it, blurConfig.width, blurConfig.height, true)
                it.recycle()
                scaled
            }
        }
    }


    /**
     * if (canvas.isHardwareAccelerated()) {
     *     val node=blur()
            canvas.drawRenderNode(node);
            //node.discardDisplayList();
         }
     */
    @RequiresApi(Build.VERSION_CODES.S)
    fun blurWithHardware(bitmap: Bitmap, blurRadius: Float): RenderNode {
        val node = RenderNode("BlurViewNode")
        //val lastBlurRadius = blurRadius
        var height = 0
        var width  = 0
        if (bitmap.height != height || bitmap.width != width) {
            height = bitmap.height
            width = bitmap.width
            node.setPosition(0, 0, width, height)
        }
        val canvas: Canvas = node.beginRecording()
        canvas.drawBitmap(bitmap, 0f, 0f, null)
        node.endRecording()
        node.setRenderEffect(RenderEffect.createBlurEffect(blurRadius, blurRadius, Shader.TileMode.MIRROR))
        // returning not blurred bitmap, because the rendering relies on the RenderNode
        return node
    }


    @Throws(RSRuntimeException::class)
    private fun blurWithRenderScript(bitmap: Bitmap, radius: Int): Bitmap? {
        val context = contextRef.get() ?: return null
        var blur: ScriptIntrinsicBlur? = null
        var input: Allocation? = null
        var output: Allocation? = null
        var rs: RenderScript? = null
        try {
            rs = RenderScript.create(context)
            rs.messageHandler = RenderScript.RSMessageHandler()
            input = Allocation.createFromBitmap(
                rs, bitmap, Allocation.MipmapControl.MIPMAP_NONE,
                Allocation.USAGE_SCRIPT
            )
            output = Allocation.createTyped(rs, input.type)
            blur = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs))
            blur.setInput(input)
            blur.setRadius(radius.toFloat())
            blur.forEach(output)
            output.copyTo(bitmap)
        } finally {
            rs?.destroy()
            input?.destroy()
            output?.destroy()
            blur?.destroy()
        }
        return bitmap
    }
}

internal fun Bitmap.blurred(
    blurMaker: BlurMaker,
    block: Canvas.() -> Unit
): Bitmap? {
    return blurMaker.blur(this.also {
        Canvas(this).run {
            block()
        }
    })
}

fun Bitmap.stackBlur(radius: Int): Bitmap? {

    if (radius < 1) {
        return null
    }
    val bitmap = this.copy(this.config, true)
    val w = bitmap.width
    val h = bitmap.height
    val pix = IntArray(w * h)
    bitmap.getPixels(pix, 0, w, 0, 0, w, h)
    val wm = w - 1
    val hm = h - 1
    val wh = w * h
    val div = radius + radius + 1
    val r = IntArray(wh)
    val g = IntArray(wh)
    val b = IntArray(wh)
    var rsum: Int
    var gsum: Int
    var bsum: Int
    var x: Int
    var y: Int
    var i: Int
    var p: Int
    var yp: Int
    var yi: Int
    var yw: Int
    val vmin = IntArray(max(w, h))
    var divsum = div + 1 shr 1
    divsum *= divsum
    val dv = IntArray(256 * divsum)
    i = 0
    while (i < 256 * divsum) {
        dv[i] = i / divsum
        i++
    }
    yi = 0
    yw = yi
    val stack = Array(div) { IntArray(3) }
    var stackpointer: Int
    var stackstart: Int
    var sir: IntArray
    var rbs: Int
    val r1 = radius + 1
    var routsum: Int
    var goutsum: Int
    var boutsum: Int
    var rinsum: Int
    var ginsum: Int
    var binsum: Int
    y = 0
    while (y < h) {
        bsum = 0
        gsum = bsum
        rsum = gsum
        boutsum = rsum
        goutsum = boutsum
        routsum = goutsum
        binsum = routsum
        ginsum = binsum
        rinsum = ginsum
        i = -radius
        while (i <= radius) {
            p = pix[yi + min(wm, max(i, 0))]
            sir = stack[i + radius]
            sir[0] = p and 0xff0000 shr 16
            sir[1] = p and 0x00ff00 shr 8
            sir[2] = p and 0x0000ff
            rbs = r1 - Math.abs(i)
            rsum += sir[0] * rbs
            gsum += sir[1] * rbs
            bsum += sir[2] * rbs
            if (i > 0) {
                rinsum += sir[0]
                ginsum += sir[1]
                binsum += sir[2]
            } else {
                routsum += sir[0]
                goutsum += sir[1]
                boutsum += sir[2]
            }
            i++
        }
        stackpointer = radius
        x = 0
        while (x < w) {
            r[yi] = dv[rsum]
            g[yi] = dv[gsum]
            b[yi] = dv[bsum]
            rsum -= routsum
            gsum -= goutsum
            bsum -= boutsum
            stackstart = stackpointer - radius + div
            sir = stack[stackstart % div]
            routsum -= sir[0]
            goutsum -= sir[1]
            boutsum -= sir[2]
            if (y == 0) {
                vmin[x] = (x + radius + 1).coerceAtMost(wm)
            }
            p = pix[yw + vmin[x]]
            sir[0] = p and 0xff0000 shr 16
            sir[1] = p and 0x00ff00 shr 8
            sir[2] = p and 0x0000ff
            rinsum += sir[0]
            ginsum += sir[1]
            binsum += sir[2]
            rsum += rinsum
            gsum += ginsum
            bsum += binsum
            stackpointer = (stackpointer + 1) % div
            sir = stack[stackpointer % div]
            routsum += sir[0]
            goutsum += sir[1]
            boutsum += sir[2]
            rinsum -= sir[0]
            ginsum -= sir[1]
            binsum -= sir[2]
            yi++
            x++
        }
        yw += w
        y++
    }
    x = 0
    while (x < w) {
        bsum = 0
        gsum = bsum
        rsum = gsum
        boutsum = rsum
        goutsum = boutsum
        routsum = goutsum
        binsum = routsum
        ginsum = binsum
        rinsum = ginsum
        yp = -radius * w
        i = -radius
        while (i <= radius) {
            yi = Math.max(0, yp) + x
            sir = stack[i + radius]
            sir[0] = r[yi]
            sir[1] = g[yi]
            sir[2] = b[yi]
            rbs = r1 - Math.abs(i)
            rsum += r[yi] * rbs
            gsum += g[yi] * rbs
            bsum += b[yi] * rbs
            if (i > 0) {
                rinsum += sir[0]
                ginsum += sir[1]
                binsum += sir[2]
            } else {
                routsum += sir[0]
                goutsum += sir[1]
                boutsum += sir[2]
            }
            if (i < hm) {
                yp += w
            }
            i++
        }
        yi = x
        stackpointer = radius
        y = 0
        while (y < h) {
            // Preserve alpha channel: ( 0xff000000 & pix[yi] )
            pix[yi] = -0x1000000 and pix[yi] or (dv[rsum] shl 16) or (dv[gsum] shl 8) or dv[bsum]
            rsum -= routsum
            gsum -= goutsum
            bsum -= boutsum
            stackstart = stackpointer - radius + div
            sir = stack[stackstart % div]
            routsum -= sir[0]
            goutsum -= sir[1]
            boutsum -= sir[2]
            if (x == 0) {
                vmin[y] = Math.min(y + r1, hm) * w
            }
            p = x + vmin[y]
            sir[0] = r[p]
            sir[1] = g[p]
            sir[2] = b[p]
            rinsum += sir[0]
            ginsum += sir[1]
            binsum += sir[2]
            rsum += rinsum
            gsum += ginsum
            bsum += binsum
            stackpointer = (stackpointer + 1) % div
            sir = stack[stackpointer]
            routsum += sir[0]
            goutsum += sir[1]
            boutsum += sir[2]
            rinsum -= sir[0]
            ginsum -= sir[1]
            binsum -= sir[2]
            yi += w
            y++
        }
        x++
    }
    bitmap.setPixels(pix, 0, w, 0, 0, w, h)
    return bitmap
}

class ViewToBlurBitmapCreator(
    private val contentView: View,
    private val onBlurBitmapCreated: (bitmap: Bitmap?, x: Float, y: Float) -> Unit
) {
    private var generateJob: Job? = null
    private var updateVersion = 0

    fun run(radius: Int, partProvider: ((w: Int, h: Int) -> Rect)?){
        generateJob?.cancel()
        updateVersion++
        if(radius == 0){
            onBlurBitmapCreated(null, 0f, 0f)
        }
        val safeRadius = radius.coerceAtMost(25)
        val currentVersion = updateVersion
        OneShotPreDrawListener.add(contentView) {
            contentView.post {
                contentView.findViewTreeLifecycleOwner()?.apply {
                    generateJob = lifecycleScope.launch {
                        if(currentVersion != updateVersion){
                            return@launch
                        }
                        if(contentView.width <= 0 || contentView.height <= 0){
                            Timber.d( "blur ignored because of size issue(w=${contentView.width}, h=${contentView.height})")
                            // retry
                            run(safeRadius, partProvider)
                            return@launch
                        }
                        try {
                            var x = 0f
                            var y = 0f
                            val bitmap = if(partProvider == null){
                                Bitmap.createBitmap(contentView.width, contentView.height, Bitmap.Config.ARGB_8888).also {
                                    val canvas = Canvas(it)
                                    contentView.draw(canvas)
                                }
                            } else {
                                val part = partProvider(contentView.width, contentView.height)
                                val w = part.width()
                                val h = part.height()
                                if (w <= 0 || h <= 0 ||
                                    part.left < 0 || part.top < 0 || part.right > contentView.width || part.bottom > contentView.height
                                ) {
                                    throw IllegalStateException("part is illegal")
                                }
                                x = part.left.toFloat()
                                y = part.top.toFloat()
                                Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888).also {
                                    val canvas = Canvas(it)
                                    canvas.translate(-x, -y)
                                    contentView.draw(canvas)
                                }
                            }
                            val blurImage = withContext(Dispatchers.IO) {
                                bitmap.stackBlur(safeRadius)?:bitmap
                            }
                            if(currentVersion == updateVersion){
                                onBlurBitmapCreated(blurImage, x, y)
                            }
                        } catch (e: Throwable) {
                            if(e !is CancellationException){
                                Timber.e( "blur image failed", e)
                            }
                        }
                    }
                }
            }
        }
        contentView.invalidate()
    }
}